import sys, os, re, types, time, glob
from xml.dom import minidom
import urlgrabber.grabber
from urlgrabber.grabber import URLGrabber
from urlgrabber.progress import TextMeter
import zipfile
import pickle

class Version:
    "Holds a supported version number"
    def __init__(self):
        self.versionString = ""
        self.major         = 0
        self.minor         = 0
        self.bugfix        = 0
        self.printString   = "No Version Information"
    
    def setVersion(self,vstring):
        versionTuple = re.compile(r'''
              # ignore beginning bullshit
        (\d+) # any number of digits
        \.    # a single '.'
        (\d+) # another set of digits
        \.*   # another optional '.'
        (\d*) # a final set of digits, or not
        .*    # anything can come after it as well
        ''', re.VERBOSE)
        if versionTuple.search(vstring):
            self.versionString = vstring
            (self.major,self.minor,self.bugfix) = versionTuple.search(vstring).groups()
            self.printString = ".".join(v for v in [self.major,self.minor,self.bugfix]
                                            if v is not '')
        else:
            self.printString = "UVI: " + vstring #"Unsupported Version Information"
            return False
    
    def __cmp__(self,cmpVersion):
        if isinstance( cmpVersion, Version ):
            if cmp(self.major,cmpVersion.major) is not 0:
                return cmp(self.major,cmpVersion.major)
            elif cmp(self.minor,cmpVersion.minor) is not 0:
                return cmp(self.minor,cmpVersion.minor)
            else:
                return cmp(self.bugfix,cmpVersion.bugfix)
        else:
            # compare them as numbers ( 1.3.5 => 100300500 > cmpVersion )
            return cmp("%03d%03d%03d" % (self.major,self.minor,self.bugfix),cmpVersion)
                



class QueueMember:
    "All the data to queue up a download"
    def __init__(self):
        self.url = ""
        self.addOn = None

class Index:
    "An index of AddOns"
    def __init__(self):
        self.isInstalled = False # This index is the installed addons index
        self.path = "" # the url path to all the file downloads
        self.addOns = []
    
    def hasAddOn(self,addon):
        "Is there an addon with the Title or Package 'addon' in this index?"
        for addOn in self.addOns:
            if addon == addOn.Title:
                return True
            elif addon == addOn.Package:
                return True
        return False
        
    def getAddOn(self,addon):
        "Get the AddOn object with the Title or Package 'addon' from this index"
        for addOn in self.addOns:
            if addon == addOn.Title:
                return addOn
            if addon == addOn.Package:
                return addOn
        return False

    def removeAddOn(self,addon):
        """Remove this addon from the Index.
           If the client has it installed, zip up the install directory and move it to:
             wow-get/packages/old/<date>/{addon.Package}.zip"""
        # if this is true, then this index is a representation of the installed mods
        # we want to back this one up
        if self.isInstalled is True:
            if self.hasAddOn(addon):
                rAddOn = self.getAddOn(addon)
            # see if there is a folder for today yet, if not, create it
            today = time.strftime("%d-%m-%Y", time.localtime())
            rDir = os.path.join("packages","removed",today)
            if not os.path.exists(rDir):
                os.mkdir(rDir)
            # compress the currently installed addon and move it to the directory
            zf = zipfile.ZipFile(os.path.join(rDir,rAddOn.Package+".zip"), "w")
            for name in glob.glob(self.path + rAddOn.Package + "/*"):
                print name, os.path.basename(name)
                zf.write(name, os.path.basename(name), zipfile.ZIP_DEFLATED)
            zf.close()
            # delete the directory that the file is currently at
            if ( rAddOn.Package.find("..") or rAddOn.Package.find("~") ) > -1:
                print "Found a malicious character in the Package name!"
                print "You should remove "+ rAddOn.Package +" manually. Aborting..."
                sys.exit(2)
            else:
                print "Removing previously installed addon: " + rAddOn.Package + "..."
                for root, dirs, files in os.walk(os.path.join(self.path,rAddOn.Package)):
                    for name in files:
                        os.remove(os.path.join(root,name))
                for root, dirs, files in os.walk(os.path.join(self.path,rAddOn.Package)):
                    for name in dirs:
                        os.rmdir(os.path.join(root,name))
                os.rmdir(os.path.join(self.path,rAddOn.Package))
                        
        # Remove the record of the AddOn from this index
        self.addOns.remove(self.getAddOn(addon))
    
    def list(self,detail="Notes"):
        "displays a list of addons in the index"
        noVersion = 0
        self.alphabetize()
        for addon in self.addOns:
            display = addon.Package or addon.Title
            if detail is "Notes":
                print "%-20s - %s" % (display,addon.Notes)
            elif detail is "Version":
                print "%-20s - %s" % (display,addon.Version.printString)
                if addon.Version.printString == "No Version Information":
                    noVersion += 1
        if detail is "Version":
            print str(len(self.addOns)) + " total addons"
            print str(noVersion) + " addons with NO version information"

    def alphabetize(self):
        self.addOns.sort(AddOn.alphaCmp)
    
    def readURL(self,source):
        "Build an index from a URL source"
        g = URLGrabber()
        try:
            sourceData = g.urlopen(source)
        except urlgrabber.grabber.URLGrabError, e:
             print 'While trying to read ' + source + ' the following occured'
             print e
             sys.exit(2)
        # sourceData = urllib.urlopen( source )
        xmldoc = minidom.parse( sourceData )
        addOns = xmldoc.getElementsByTagName("Addon")
        self.path = xmldoc.firstChild.getAttribute("Path")
        # read the xml and create addOn records to place into the new index
        for addOn in addOns:
            # create a new addOn object
            newAddOn = AddOn()
            newAddOn.readXML(addOn)
            self.addOns.append(newAddOn)

class AddOn:
    "A single AddOn"
    def __init__(self):
        self.File          = ""
        self.Notes         = ""
        self.Author        = ""
        self.Title         = ""
        self.Interface     = ""
        self.Version       = Version()
        self.VersionString = ""
        self.Package       = ""
        self.Dependencies  = []
        self.OptionalDeps  = []
       
    def __cmp__(self, addon):
        if isinstance( addon, AddOn ):
            if self.Interface != addon.Interface:
                return cmp(self.Interface,addon.Interface)
            elif self.Version and addon.Version:
                return cmp(self.Version, addon.Version)
        else:
            if self.Interface != addon:
                return cmp(self.Interface,addon)
            elif self.Version:
                return cmp(self.Version, addon)

    def alphaCmp(self, addon):
        if isinstance(addon, AddOn):
            return cmp(self.Package or self.Title,addon.Package or addon.Title)
        else:
            return cmp(self.Package or self.Title,addon)        

    def __str__(self):
        retStr = "%s\n" % self.Title
        retStr += "Addon    : %s\n" % self.Package
        retStr += "Interface: %s\n" % self.Interface
        retStr += "Version  : %s\n" % self.VersionString
        retStr += "Notes    : %s\n" % self.Notes
        retStr += "Author   : %s" % self.Author # leave the last \n off
        return retStr
    
    def readXML(self,xml):
        for child in xml.childNodes:
            if child.nodeType == child.ELEMENT_NODE:
                if child.tagName == "File":
                    try:
                        self.File = child.firstChild.data.strip()
                    except AttributeError:
                        self.File = ""
                elif child.tagName == "Notes":
                    try:
                        self.Notes = child.firstChild.data.strip()
                    except AttributeError:
                        self.Notes = ""
                elif child.tagName == "Author":
                    try:
                        self.Author = child.firstChild.data.strip()
                    except AttributeError:
                        self.Author = ""
                elif child.tagName == "Title":
                    try:
                        self.Title = child.firstChild.data.strip()
                    except AttributeError:
                        self.Title = ""
                elif child.tagName == "Interface":
                    try:
                        self.Interface = child.firstChild.data.strip()
                    except AttributeError:
                        self.Interface = 0
                elif child.tagName == "Version":
                    try:
                        self.VersionString = child.firstChild.data.strip()
                        self.Version.setVersion(self.VersionString)
                    except AttributeError:
                        self.VersionString = "0.0.0"
                        self.Version.setVersion(self.VersionString)
                elif child.tagName == "Package":
                    try:
                        self.Package = child.firstChild.data.strip()
                    except AttributeError:
                        self.Package = ""
                elif child.tagName == "Dependencies":
                    for item in child.childNodes:
                        if item.nodeType == item.ELEMENT_NODE:
                            try:
                                self.Dependencies.append(item.firstChild.data.strip())
                            except AttributeError:
                                pass
                elif child.tagName == "OptionalDeps":
                    for item in child.childNodes:
                        if item.nodeType == item.ELEMENT_NODE:
                            try:
                                self.OptionalDeps.append(item.firstChild.data.strip())
                            except AttributeError:
                                pass
    def readTOC(self,toc):
        # this matches ## attribute: value
        tocPattern = re.compile(r'## (.*): (.*)')
        lines = toc.split('\n')
        for line in lines:
            pieces = tocPattern.search(line)
            if pieces is not None:
                pieces = pieces.groups()
                if pieces[0] == 'Notes':
                    self.Notes = pieces[1].strip()
                elif pieces[0] == 'Author':
                    self.Author = pieces[1].strip()
                elif pieces[0] == 'Title':
                    self.Title = pieces[1].strip()
                elif pieces[0] == 'Interface':
                    self.Interface = pieces[1].strip()
                elif pieces[0] == 'Version':
                    self.VersionString = pieces[1].strip()
                    self.Version.setVersion(self.VersionString)
                elif pieces[0] in ('Dependencies','RequiredDeps','Dependancies'):
                    self.Dependencies = [f.strip() for f in pieces[1].split(',')]
                elif pieces[0] == 'OptionalDeps':
                    self.OptionalDeps = [f.strip() for f in pieces[1].split(',')]

class WoWGet:
    "This is the container class"

    def __init__(self):
        self.indices = []
        self.config = self.Configuration()
        # Try to unpickle the cached index information
        try:
            cache = open(os.path.join('cache','indices.data'),'rb')
            self.indices = pickle.load(cache)
            cache.close()
        except IOError:
            pass

    class Configuration:
        "The user's configuration."
        def __init__(self):
            self.addOnsDirectory = ""
            self.sources = []
            self.installed = Index() # mods installed
            
            # boot stuff
            self.read() # read in the conf file
            self.scanInstalled() # scan their installed mods
        
        def read(self):
            "Reads the wgconf.xml file from PWD. If any of it is missing the client will exit with an error"
            xmldoc = minidom.parse("wgconf.xml") # the conf file has to reside in the same directory as wow-get
            try:
                self.addOnsDirectory = str(xmldoc.getElementsByTagName("Directory")[0].firstChild.data).strip()
                if len(self.addOnsDirectory.strip()) < 1:
                    print 'No AddOn Directory specified. Exiting...'
                    sys.exit(2)
            except:
                self.addOnsDirectory = None
                print 'No AddOn Directory specified. Exiting...'
                sys.exit(2)
            for source in xmldoc.getElementsByTagName("Source"):
                try:
                    self.sources.append(str(source.firstChild.data))
                except:
                    print 'Empty source tag. Skipping...'
            if len(self.sources) < 1:
                print 'No sources found. Edit your wgconf.xml'
                sys.exit(2)
        
        def scanInstalled(self):
            "Scans the users addons directory and builds an index"
            self.installed.path = self.addOnsDirectory
            self.installed.isInstalled = True
            modFolders = [f for f in os.listdir(self.installed.path) 
            if os.path.isdir(os.path.join(self.installed.path, f))]
            for folder in modFolders:
                if folder.find('Blizzard_') == -1: # Default UIs don't have .toc files
                    toc = open( os.path.join(self.installed.path, folder, folder + '.toc') )
                    tocString = toc.read()
                    toc.close()
                    # convert the tocString into an AddOn object
                    newAddOn = AddOn()
                    newAddOn.readTOC(tocString)
                    newAddOn.Package = folder; # the actual Package name isn't in the .toc
                    self.installed.addOns.append(newAddOn)

    # I'm not absolutely certain about this method of providing help
    # I suppose we should break help into it's own sub directory and
    # just print out files?
    def help(self,topic="main"):
        "Prints a help topic to the user"
        topics = { "main": 
        """wow-get pre-alpha 0.01
Usage: wow-get [options] command
wow-get [options] install|remove pkg1 [pkg2 ...]

wow-get is a command line interface tool for downloading and
installing user interface mods for a popular MMORPG. The most
used commands are update and install.

Commands:
    update    - retrieve a list of available addons from all registered mod servers
    upgrade   - download the newest replacements for currently installed addons
    list      - display a list of all available addons from your sources
    detail    - display detailed information about specified addons
    install   - download and install an addon
    installed - display all installed addons on your system
    versions  - display version information about your installed addons
    remove    - remove addons from your addons folder
    clean     - delete all the downloaded addon archives (not implemented)"""
        }
   
        if topic in topics.keys():
            print topics[topic]
        else:
            print topics["main"]

    def update(self):
        "Gets latest indexes from all sources"
        # Clear out the old indices
        self.indices = []
        # read all of the index files, they are assumed to exist at source + 'index.xml'
        for source in self.config.sources:
            newIndex = Index()
            print "Downloading index information from " + source
            newIndex.readURL( source + 'index.xml' )
            self.indices.append( newIndex )
        # Pickle the indices
        cache = open(os.path.join('cache','indices.data'),'wb')
        pickle.dump(self.indices,cache)
        cache.close()

    def expand(self,word,useIndex=None):
        "Expand any * characters to match available packages"
        if word.find('*') is not -1:
            keepers = []
            pattern = re.compile(word.replace('*','(.*)'))
            if useIndex is None:
                for index in self.indices:
                    for addon in index.addOns:
                        if pattern.search(addon.Package or addon.Title):
                            keepers.append(addon.Package or addon.Title)
            # Need to make sure useIndex is a proper index object
            elif isinstance(useIndex,Index):
                for addon in useIndex.addOns:
                    if pattern.search(addon.Package or addon.Title):
                        keepers.append(addon.Package or addon.Title)
            # some kind of junk got passed in, just return the word
            else:
                return word
            # return the list of things that were found
            return keepers 
        else:
            return word
    
    def upgrade(self):
        "Downloads and installs all possible upgrades"
        upgradeList = []
        for addon in self.config.installed.addOns:
            for index in self.indices:
                if index.hasAddOn(addon.Package or addon.Title):
                    pAddOn = index.getAddOn(addon.Package or addon.Title)
                    # the __cmp__ function should handle this correctly
                    if pAddOn > addon:
                        upgradeList.append( pAddOn.Package or pAddOn.Title )
                        #print (pAddOn.Package or pAddOn.Title) + " will be upgraded..."
        if len(upgradeList) > 0:
            self.install(upgradeList)
        else:
            print "Could not find any packages to upgrade."
                    
    def list(self):
        for index in self.indices:
            index.list()

    def detail(self,addonList):
        "Prints detailed information about an addon"

        for addon in addonList:
            for index in self.indices:
                if index.hasAddOn(addon):
                    print "From " + index.path + ":"
                    print index.getAddOn(addon)
        pass

    def remove(self,addonList):
        "Tries to remove addons"
        removeList = []

        addons = []
        for item in addonList:
            eItem = self.expand(item,self.config.installed)
            if type(eItem) is types.ListType:
                addons.extend(eItem)
            else:
                addons.append(eItem)

        # Make sure these addons are even isntalled
        for addon in addons:
            if self.config.installed.hasAddOn(addon):
                removeList.append(addon)

        # look throgh ALL installed addons and see if anything the client
        # wants to uninstall has dependents. If so, add them to the uninstall
        # list... (maybe just let the user know that these addons won't work?)
        for iAddon in self.config.installed.addOns:
            for iDep in iAddon.Dependencies:
                if iDep in addons:
                    if iAddon.Package or iAddon.Title not in removeList:
                        removeList.append(iAddon.Package or iAddon.Title)
        if len(removeList) > 0:
            print "The following addons have been queued for removal:"
            print "   " + " ".join(removeList)
        else:
            print "Nothing to remove, exiting..."
            sys.exit()
        # Prompt the user
        print "Accept these changes? [y/N]: ",
        confirmed = False
        while 1:
            next = sys.stdin.read(1)
            if not next:
                break;
            elif next in ('y','Y'):
                confirmed = True
                break;
            else:
                break;
        if not confirmed:
            print "Aborting..."
            sys.exit()
        # Go ahead and nuke the addons
        for rAddOn in removeList:
            self.config.installed.removeAddOn(rAddOn)
    
    def install(self,addonList):
        "Tries to install addons"
        installQueue = []
        installList = [] # used for checking duplicates
        dependencyQueue = []
        depList = []     # used for checking duplicates
        optionalQueue = []
        optList = []     # keep up with optional packages
        optionals = []   # list of potential optional packages
        unpack = []      # list of zips to unpack
        # do expansion on addonList
        addons = []
        for item in addonList:
            eItem = self.expand(item)
            if type(eItem) is types.ListType:
                addons.extend(eItem)
            else:
                addons.append(eItem)
        # test an expansion to detect if this is large package of addons
        for addon in addons:
            word = addon + "*"
            expTest = self.expand(word)
            if type(expTest) is types.ListType:
                for item in expTest:
                    if item not in addons:
                        optionals.append(item)
        # --------------------------------------------------------------

        for addon in addons:
            # we've already added this addon, skip
            if addon in installList:
                continue
            if addon in depList:
                continue
            found = False # if this doesn't get set to True we bail
            # First, search for all the addons they listed
            for index in self.indices:
                if index.hasAddOn( addon ) is True:
                    found = True
                    myAddon = index.getAddOn( addon )
                    # See if they have an equivalent or newer version installed
                    if self.config.installed.hasAddOn( addon ):
                        if self.config.installed.getAddOn( addon ) > myAddon:
                            continue
                    installList.append( myAddon.Package or myAddon.Title )
                    myQueueMember = QueueMember()
                    myQueueMember.url = index.path + myAddon.File
                    myQueueMember.addOn = myAddon
                    installQueue.append(myQueueMember)
            if found is False:
                print "Sorry. The addon '" + addon + "' is not available from any of your sources\n"
                return
            # Second, try to add dependencies, sigh this is so loopy
            for queuedMember in installQueue:
                if len(queuedMember.addOn.Dependencies) > 0:
                    for dependency in queuedMember.addOn.Dependencies:
                        # If we have already added this dependency skip it
                        if dependency in depList:
                            continue
                        if dependency in installList:
                            continue
                        # Is this addon already installed? If so skip it
                        if self.config.installed.hasAddOn( dependency ):
                            continue
                        found = False # if this doesn't get reset to True we bail
                        for index in self.indices:
                            if index.hasAddOn( dependency ):
                                found = True
                                myAddon = index.getAddOn( dependency )
                                depList.append( myAddon.Package or myAddon.Title ) # so we don't add it more than one time
                                myDep = QueueMember()
                                myDep.url = index.path + myAddon.File
                                myDep.addOn = myAddon
                                dependencyQueue.append( myDep )
                        if found is False:
                            print "Sorry. Could not satisfy " + queuedMember.addOn.Package or queuedMember.addOn.Title + "'s dependency for " + dependency + "..."
                            return
        # Print out what we are going to do
        if( len(installList) > 0):
            print "The following addons are queued to be installed:"
            print "   ", " ".join(installList)
        else:
            print "Nothing to install..."
            sys.exit()
        if( len(depList) > 0 ):
            print "Additionally, the following packages will be installed:"
            print "   ", " ".join(depList)
        if( len(optionals) > 0 ):
            print "These optional packages are recommended:"
            print "   ", " ".join(optionals)
        # Prompt the user
        print "Accept these changes? [y/N]: ",
        confirmed = False
        while 1:
            next = sys.stdin.read(1)
            if not next:
                break;
            elif next in ('y','Y'):
                confirmed = True
                break;
            else:
                break;
        if not confirmed:
            print "Aborting..."
            sys.exit()
        # lets grab the files and drop them in "packages/"
        destination = 'packages/'
        grabber = URLGrabber(progress_obj=TextMeter())
        
        # glue the dependency queue to the installQueue
        installQueue.extend(dependencyQueue)
        
        for installQM in installQueue:
            try:
                grabber.urlgrab(installQM.url,destination+installQM.addOn.File)
                unpack.append(installQM.addOn.File)
            # Bail out if we run into an error
            except urlgrabber.grabber.URLGrabError, e:
                print e
                sys.exit(2)
                
        # Now all we have left to do is unpack the new addons into the addon directory
        for package in unpack:
            print "Installing " + package + "..."
            zip = zipfile.ZipFile(os.path.join("packages",package),'r')
            # Get the destination dir
            testDir = package.replace(".zip","") #bad variable name :/
            # if this is true we need to uninstall the pre-existing mod, as this is
            # an upgrade
            if os.path.exists(os.path.join(self.config.addOnsDirectory,testDir)):
                self.config.installed.removeAddOn(testDir)
                
            for name in zip.namelist():                
                # catch exceptions here...
                if ( name.find("..") or name.find("~") ) > -1:
                    print package + " has a maliciously named file inside, aborting..."
                    sys.exit(2)
                if name.endswith('/'):
                    os.mkdir(os.path.join(self.config.addOnsDirectory,name.replace("/",os.sep)))
                else:
                    outfile = open(os.path.join(self.config.addOnsDirectory, name.replace("/",os.sep)), 'wb')
                    outfile.write(zip.read(name))
                    outfile.close()
            # remove the archive (or maybe move it to archive/?
            zip.close()
            os.remove(os.path.join("packages",package))
            
# End WoWGet Container Class -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

if __name__ == "__main__":
    print "This module cannot be run on its own"
